package org.devtcg.five.music.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLDecoder;
import java.util.HashSet;
import java.util.Locale;

import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.RequestLine;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.devtcg.five.provider.util.Sources;
import org.devtcg.five.service.ICacheService;

import android.content.ContentResolver;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;

public class LocalHttpServerHack extends LocalHttpServer
{
	private final HttpClient mClient;
	private final SongDownloadHandler mHandler;
	private final ContentResolver mResolver;
	private final ICacheService mCache;

	private HashSet<HttpRequestBase> mDownloads =
	  new HashSet<HttpRequestBase>();

	public LocalHttpServerHack(int port, SongDownloadHandler handler,
	  ContentResolver resolver, ICacheService service)
	  throws IOException
	{
		super(port);
		setRequestHandler(mHttpHandler);
		mClient = new DefaultHttpClient();
		mHandler = handler;
		mResolver = resolver;
		mCache = service;
	}

	@Override
	public void shutdown()
	{
		/* TODO: We need to call mHandler.sendAborted() somehow. */
		synchronized(mDownloads) {
			for (HttpRequestBase req: mDownloads)
				req.abort();

			mDownloads.clear();
		}
		
		super.shutdown();
	}

	/**
	 * Craft a URL we can point the media player toward for the specified
	 * resource. 
	 */
	public static String makeUri(long sourceId, long contentId, long songId)
	{
		return "http://127.0.0.1/" + sourceId + "/" + contentId +
		  "/" + songId;
	}

	/* This will see a string like "/1/145". */
	protected static FiveSong parseUri(String uri)
	  throws IllegalArgumentException
	{
		String[] segments = uri.split("/");

		if (segments.length != 4)
			throw new IllegalArgumentException();

		long sourceId = Long.parseLong(segments[1]);
		long contentId = Long.parseLong(segments[2]);
		long songId = Long.parseLong(segments[3]);

		return new FiveSong(songId, sourceId, contentId);
	}

	private final HttpRequestHandler mHttpHandler = new HttpRequestHandler()
	{
		public void handle(HttpRequest request, HttpResponse response,
		  HttpContext context)
		  throws HttpException, IOException
		{
			RequestLine reqLine = request.getRequestLine();
			Log.v(TAG, "reqLine=" + reqLine);

			String method = reqLine.getMethod().toUpperCase(Locale.ENGLISH);
			if (method.equals("GET") == false)
			{
				throw new MethodNotSupportedException(method +
				  " method not supported");
			}

			Log.v(TAG, "request=" + request.toString());

			String target = reqLine.getUri();
			FiveSong c;

			try {
				c = parseUri(target);
			} catch (IllegalArgumentException e) {
				Log.e(TAG, "Unable to parse Uri: " + target);
				throw e;
			}

			try {
				response.setEntity(new FiveHackEntity(c));
				response.setStatusCode(HttpStatus.SC_OK);
			} catch (Exception e) {
				Log.e(TAG, "Ugh", e);
				response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
			}
		}
	};

	private static class FiveContent
	{
		public long sourceId;
		public long contentId;

		public FiveContent(long sourceId, long contentId)
		{
			this.sourceId = sourceId;
			this.contentId = contentId;
		}

		@Override
		public String toString()
		{
			StringBuilder b = new StringBuilder();

			b.append("{ ");
			b.append("sourceId=").append(sourceId).append(", ");
			b.append("contentId=").append(contentId);
			b.append(" }");

			return b.toString();
		}

		@Override
		public boolean equals(Object o)
		{
			if (!(o instanceof FiveContent))
				return super.equals(o);

			FiveContent oo = (FiveContent)o;

			if (oo.sourceId != sourceId)
				return false;

			if (oo.contentId != contentId)
				return false;

			return true;
		}
	}
	
	private static class FiveSong extends FiveContent
	{
		public long songId;
		
		public FiveSong(long songId, long sourceId, long contentId)
		{
			super(sourceId, contentId);
			this.songId = songId;
		}
	}

	private class FiveHackEntity extends TeedInputStreamEntity
	{
		private final HttpContext mContext;
		private final FiveSong mContent;
		
		private long mBytes = 0;

		public FiveHackEntity(FiveSong c)
		  throws Exception
		{
			super();

			mContent = c;

			HttpRequestBase req = null;
			OutputStream out = null;
			
			mHandler.sendBegin(c.songId);

			try {
				HttpContext ctx = getFiveContent(c);
				assert ctx != null;

				req = (HttpRequestBase)ctx
				  .getAttribute(ExecutionContext.HTTP_REQUEST);
				assert req != null;
				assert mDownloads.contains(req) == true;

				HttpResponse resp = (HttpResponse)ctx
				  .getAttribute(ExecutionContext.HTTP_RESPONSE);
				assert resp != null;
				
				HttpEntity ent = resp.getEntity();
				assert ent != null;
				
				long length = ent.getContentLength();
				assert length != 0;
				
				if (length > 0)
					mHandler.sendSetLength(c.songId, length);

				InputStream in = ent.getContent();
				out = openFiveCache(c);

				setStream(in, out, -1);				
				mContext = ctx;
			} catch (Exception e) {
				if (req != null)
				{
					mDownloads.remove(req);

					if (req.isAborted() == false)
						req.abort();
				}

				if (out != null)
					try { out.close(); } catch (Exception e2) {}

				mHandler.sendError(c.songId, e.toString());

				throw e;
			}
		}

		private HttpContext getFiveContent(FiveContent c)
		  throws Exception
		{
			HttpContext ctx = new BasicHttpContext();

			URL url = Sources.getContentURL(mResolver,
			  c.sourceId, c.contentId);

			HttpGet method = new HttpGet(url.toString());

			if (isInterrupted() == true)
				throw new InterruptedException();

			synchronized(mDownloads) {
				mDownloads.add(method);
			}

			try {
				HttpResponse resp = mClient.execute(method);

				if (isInterrupted() == true)
					throw new InterruptedException();

				StatusLine status = resp.getStatusLine();

				if (status.getStatusCode() != HttpStatus.SC_OK)
				{
					throw new IllegalStateException("Unexpected server response: "
					  + status);
				}

				if (resp.getEntity() == null)
				{
					throw new IllegalStateException("Unexpected server response: "
					  + "no response entity.");
				}

				ctx.setAttribute(ExecutionContext.HTTP_REQUEST, method);
				ctx.setAttribute(ExecutionContext.HTTP_RESPONSE, resp);

				return ctx;
			} catch (Exception e) {
				if (method.isAborted() == false)
					method.abort();

				synchronized(mDownloads) {
					mDownloads.remove(method);
				}

				throw e;
			}
		}

		public OutputStream openFiveCache(FiveContent c)
		  throws RemoteException
		{
			ParcelFileDescriptor pd =
			  mCache.requestStorage(c.sourceId, c.contentId);

			return new ParcelFileDescriptor.AutoCloseOutputStream(pd);
		}

		@Override
		public void onWrite(int n)
		{
			assert n >= 0;

			if (n > 0)
			{
				mBytes += n;
				mHandler.sendOnRecv(mContent.songId, mBytes);
			}
		}

		@Override
		public void onConsumed(boolean success)
		{
			try {
				mIn.close();
			} catch (IOException e) {}

			try {
				mOut.close();
			} catch (IOException e) {}

			HttpRequestBase req = (HttpRequestBase)mContext
			  .getAttribute(ExecutionContext.HTTP_REQUEST);
			assert req != null;

			synchronized(mDownloads) {
				mDownloads.remove(req);
			}

			if (success == true)
			{
				mHandler.sendFinished(mContent.songId);
			}
			else
			{
				mHandler.sendError(mContent.songId,
				  "FIXME: No idea what the error was");

				Log.v(TAG, "Releasing cache of " + mContent);

				try {
					mCache.releaseStorage(mContent.sourceId,
					  mContent.contentId);
				} catch (RemoteException e) {}
			}
		}
	}
}
